RMV5 - Documentation
---------------------------------------------------------------------------------------------------------------
By Kypo
About RMV5:
Recoil Master v5 is the latest version of the client, released September 14, 2025– the client’s initial release (v1) on April 9th 2025.
How it works on a technical level:
RMV5 uses “"ctypes"”, specifically a legacy version of it. "ctypes" is a built-in Python library that lets you directly call functions from Windows DLLs (or shared libraries on Linux/Mac). Think of it as a thin bridge between Python and low-level system APIs written in C.
Using this python library means the script isn’t what’s moving the mouse or detecting whether the mouse is clicked/held down but instead it manipulates Windows at the operating system level to simulate key presses, mouse inputs and movements, etc.
The "ctypes" library with user32.dll calls are closer to “real” system calls. The game just sees “Windows moved the cursor,” not “an external app faked a mouse event.”
In the client there are x and y amounts (in whole numbers because it’s pixels that the mouse is moving, not subpixels-- that’s an OS level thing) and a time delay. Every time that the time delay has been passed and the right and left mouse buttons are being held down, the client moves the mouse down by the given y amount and has a random chance of moving the x amount:
def recoil_control():
nonlocal paused
nonlocal weapon
while True:
if weapon == 1:
with pause_lock:
if holding and not paused:
dx = variable2 if random.random() < 0.5 else 0
move_mouse(dx, variable3)
time.sleep(variable1)
else:
with pause_lock:
if holding and not paused:
dx = variable5 if random.random() < 0.5 else 0
move_mouse(dx, variable6)
time.sleep(variable4)
The above code shows how it randomly applies the x value since x recoil is usually barely as strong as y recoil in the game. This randomness also helps avoid detection as it makes the pull down effect more natural and less simulated. This is applied in the script with the function move_mouse() with inputs for the change in x and change in y, which is then sent to Windows’ API:
def move_mouse(dx, dy):
ctypes.windll.user32.mouse_event(0x0001, int(dx), int(dy), 0, 0)
Using the Client:
On the start page of the client, the user has two options -- either loading an existing save or starting from scratch (default settings and no set configs for any operators). Selecting “Start from Scratch” will take you to the operator select menu. You can find an operator from this page and apply, edit, and change their recoil settings by clicking on them. You can also middle click on your mouse to add it to the favorites tab for easy access when switching operators.
Upon clicking on an operator, whichever settings you have selected to that operator are applied. Any time that a variable is changed, that change is automatically updated and sent to the python side of the client to be used. As mentioned above, the X and Y values for each gun are moved every time the time delay variable has passed. So for example, if you have Ash selected, and set the X value to -1, the Y value to -6, and the time delay to 0.018 seconds, then every time the left and right mouse buttons are held down the client will move the mouse ≈-0.5 pixels horizontally and -6 pixels vertically every 0.018 seconds (18ms).
The horizontal movement is roughly half of the X value selected in the client because of the randomness built into the client mentioned earlier. The colored bullseye visualizer gives an example of what this recoil control pattern theoretically looks like and gives a few extra calculations on the side to help understand how the variables will function. Note how the slope next to the visualizer is 12 instead of 6. Again, this is because of the X value actually equating to be about 0.5 of its actual value.
Clicking the secondary button allows you to modify settings of the secondary weapon. This looks and functions exactly the same way as the primary weapon, but it allows users to set up varying settings for both of the operators weapons:
Rapid Fire:
Right under the visualization tab is the Rapid Fire button. Clicking this button allows you to switch the rapid fire option for this operator to be off, on for weapon 1, or on for weapon 2. In most cases, it’s nice to have it on for weapon 2 since most operators have a single shot weapon as their secondary (only 1 shot fired per click). In some cases however, it’s nice to have it on for weapon 1.
My favorite example of this is the operator Glaz since his primary gun is already overpowered as it is able to highlight enemies through smoke or just out in the open bright yellow. Having rapid fire on for weapon 1 on glaz, as well as being able to negate
his recoil by adjusting the recoil variables on his primary in the client, gives you a fully automatic thermal-scoped rifle with little to no recoil:
The rapid/auto fire feature is something that is completely new to Recoil Master, only being added in version 5, although it has been in development since version 2. It has only been made possible because the part of the script that clicks the mouse down works at the OS level, which is one of the only things that Rainbow Six Siege will listen to and actually perceive as a valid input (countless input methods have been tested and failed before getting to this point).
def rapid_fire():
nonlocal weapon
nonlocal holding
while True:
if rapid_fire_enabled == weapon and holding:
time.sleep(0.001)
while key_down(MOUSEEVENTF_LEFTDOWN):
click_mouse()
time.sleep(random.uniform(0.011, 0.023))
#time interval between clicks
else:
time.sleep(0.1)
In the code above, you can see how this is executed. If the rapid_fire_enabled variable is 1 it’s for the primary, 2 it’s for the secondary, and 0 which is the default it will be disabled. You can also see in the code above that there again is some randomness occurring; the mouse click is simulated and then the function sleeps for a time ranging from 0.011 seconds to 0.023 seconds. This randomness is again to simulate natural behavior and to not get flagged as suspicious. The “holding” variable is either true or false and is based on if the user is holding down the left and right mouse buttons. This has to be equal to true in order for rapid fire to occur-- hence why it only functions when the user is aiming down scope. A mouse click is simulated like this:
def click_mouse():
nonlocal user32, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTDOWN
user32.mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.015)
user32.mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
Spam Crouching:
Spam crouching is another new feature that came to Recoil Master in version 5. It functions very similarly to the rapid fire macro but instead of being enabled or disabled, it’s always enabled and waiting for the spam crouch hotkey to activate. By default, the spam crouch hotkey is the “k” key on the keyboard. This can be changed as well as other settings in the client itself in the settings tab:
The client listens for the crouch button to be pressed using this function:
def crouch_listener():
while True:
VK_K = ord(crouch_key.upper())
if key_down(VK_K):
press_c()
time.sleep(random.uniform(0.01, 0.03))
The “crouch_key” variable is primarily set to “k” in the client by default, and the variable is a string:
crouch_key = 'k'
Python converts the string with the method .upper() to make it an uppercase string; turning ‘k’ into ‘K’, then putting ‘K’ through ord() takes the character and outputs it’s unicode code point (it’s number ID). For ‘K’, the output unicode value is 75. Why do we need the unicode value? It goes back to the method of which keys and clicks are being simulated.
Since we’re using an OS level interaction library, it requires exact unicode values in order to accurately simulate button presses and clicks as well as detect button presses. This is how any key that is pressed is detected:
def key_down(vk):
nonlocal user32
return (user32.GetAsyncKeyState(vk) & 0x8000) != 0
For the crouch spammer, whenever the keydown() is equal to the “crouch_key” variable, it turns on the spam function which essentially just repeatedly presses the “c” button on the user’s computer:
def crouch_listener():
while True:
VK_K = ord(crouch_key.upper())
if key_down(VK_K):
press_c()
time.sleep(random.uniform(0.01, 0.03))
def press_c():
nonlocal user32, VK_C, SC_C, KEYEVENTF_KEYUP
# Press
user32.keybd_event(VK_C, SC_C, 0, 0)
# Release
time.sleep(0.02)
user32.keybd_event(VK_C, SC_C, KEYEVENTF_KEYUP, 0)
Again, note the random.uniform() in the crouch_listener initiator function. This is just for anti-detection purposes.
Custom Crosshairs
Custom crosshairs in Recoil Master allows you to select from 200+ pre-loaded custom reticle sprites which, on selection, displays the crosshair in the middle of the user’s main monitor as a basic transparent overlay. This is why the game itself has to be in either borderless or fullscreen; the crosshair cannot overlay a fullscreen application, and sometimes the recoil controller script and other things break if the user has settings prioritizing fullscreen applications.
The crosshairs page is right next to the operator select page on the menu slider and upon opening this page, the user is shown 14-15 crosshairs per page. Pages can be switched using the dots below the crosshairs. On the right side of the client on this page, there are settings for the crosshair. The opacity slider allows the user to change how transparent the crosshair is and the size slider allows the user to change how large it appears. The crosshair’s color can also be set using the color tint button, which shows a color picker allowing for infinite customizations.
The python side of the client first gets the user’s main monitor, then finds it’s height and width in order to find the center and properly scale the crosshair image. It then removes transparent pixels in order to remove the invisible background of the given crosshair and then finally saves this image as a temporary file. Once this is completed, it fades in the crosshair image at the center of the user’s main monitor. Here is how it does this exactly:
original_image = QtGui.QPixmap(image_path)
if original_image.isNull():
raise ValueError("Failed to load image from input (base64 or file path).")
if grayscale:
image = original_image.toImage().convertToFormat(QtGui.QImage.Format_ARGB32)
original_image = QtGui.QPixmap.fromImage(image)
self.opacity = max(0.0, min(opacity, 1.0))
monitor = next((m for m in get_monitors() if m.is_primary), get_monitors()[0])
screen_x = monitor.x
screen_y = monitor.y
screen_w = monitor.width
screen_h = monitor.height
if grayscale:
new_width = int(original_image.width() * (size_percent / 100))
new_height = int(original_image.height() * (size_percent / 100))
self.image = original_image.scaled(new_width, new_height, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
x = screen_w // 2 - self.image.width() // 2
y = screen_h // 2 - self.image.height() // 2
self.setGeometry(screen_x + x, screen_y + y, self.image.width(), self.image.height())
self.setFixedSize(self.image.width(), self.image.height())
else:
self.image = original_image
self.setGeometry(screen_x, screen_y, self.image.width(), self.image.height())
self.setFixedSize(self.image.width(), self.image.height())
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint |
QtCore.Qt.WindowStaysOnTopHint |
QtCore.Qt.Tool |
QtCore.Qt.WindowTransparentForInput
)
self.setWindowOpacity(0.0)
self.show()
self.fade_in()
This takes in a dataURL of the crosshair image and parses it into the temp file. This means that you can pretty much send it any dataURL and it will try to display the image on screen. This allows users to be able to select “Import Custom” on the first page of the crosshairs menu and upload any picture from their computer. The client compacts it, adds it to the crosshair pages (first page) and then when selected, it sends that image dataURL to the python client. In the main settings menu (not the crosshair settings) there is an option to turn on outlines for crosshairs:
This works with most of the default ones, but can look weird on some imported ones depending on the default colors the image came with as well as its geometry, which is why it’s labeled in settings as being glitchy because it doesn’t always look the best.
Default in-game crosshairs and reticles can be disabled in siege, as well as the optic color (can be turned down to super low opacity and set to black, making it almost disabled). Doing this means you can essentially replace the default crosshair in the game with whichever one you want from the client.
Custom crosshairs have been noted by many professional gamers as well as casual players to help improve their skills in game significantly by allowing them to have consistent visibility of where it is that they are looking, aiming, and shooting. It also makes it possible to get clear indication of aiming direction on weapons in-game that have worse scopes that make it harder to see.
How the Web Client and Python Script Communicate:
The web client and python client communicate via a webhook on port 6741. Inside the python script, there is a webhook message handler that is able to send and receive messages on this port. This data is only transferred locally on the user’s computer. The web client also has a websocket handler. The web version is written as a javascript extension and is able to tell if the two are connected to each other, send messages, read the last message sent from the python client, and set itself to the same port. The python version of the handler works like this:
async def handler(websocket):
nonlocal variable1, variable2, variable3, variable4, variable5, variable6, paused, ws_client
nonlocal color, grayscale, size_percent, png
nonlocal overlay_widget
nonlocal weapon
nonlocal rapid_fire_enabled
print("Client connected to WebSocket")
with ws_lock:
ws_client = websocket
Sending a message to the web client:
async def _send_ws_message(message):
nonlocal ws_client
try:
if ws_client:
await ws_client.send(message)
print(f"Sent message: {message}")
except Exception as e:
print(f"WebSocket send error: {e}")
Threadsafe message send:
def send_ws_message_threadsafe(message):
asyncio.run_coroutine_threadsafe(_send_ws_message(message), loop)
And the websocket_server() function works like this:
async def websocket_server():
async with websockets.serve(handler, "localhost", 6741):
print("WebSocket server listening on ws://localhost:6741")
await asyncio.Future()
It is initiated upon starting the client with this function:
def start_websocket_server():
asyncio.set_event_loop(loop)
loop.run_until_complete(websocket_server())
Here is a view of how the web client works (javascript):
connect(args) {
const url = args.url;
if (this.socket) {
this.socket.close();
}
this.socket = new WebSocket(url);
this.socket.onopen = () => {
console.log("[PythonBridge] Connected");
this.connected = true;
};
this.socket.onmessage = (event) => {
this.lastMessage = event.data;
console.log("[PythonBridge] Received:", event.data);
};
this.socket.onerror = (error) => {
console.error("[PythonBridge] Error:", error);
this.connected = false;
};
this.socket.onclose = () => {
console.log("[PythonBridge] Connection closed");
this.connected = false;
};
}
sendMessage(args) {
const msg = args.message;
if (this.socket && this.connected) {
this.socket.send(msg);
} else {
console.warn("[PythonBridge] Not connected");
}
}
getLastMessage() {
return this.lastMessage;
}
isConnected() {
return this.connected;
}
Save Files:
The third tab of the client opens the save files tab. Downloading a save file opens a window to save the user’s current config file, which now in v5 not only includes operator settings, but hotkeys, custom crosshairs, and more.
The “Import Existing” button allows you to load an existing save file from your computer (including legacy save files) and automatically sets all settings and operator configurations to the selected values in that config file. The settings on the right side allow you to pick and choose what all is loaded when a save file is uploaded. When saving a config from the client, the web client must send this save file over the local webhook on port 6741 to the python script since the web client is in a python webview window.
Webview windows can have files uploaded into them, but they have lots of strict limitations regarding other things (such as downloading files to the user's computer). Save files are just text files and can be even opened and edited in a notepad editor on your computer and then uploaded into the client.
More Resources:
Full development showcases and tutorials are on my youtube channel, Kypo, located at: (93) Kypo! - YouTube And a 20+minute tutorial and showcase for v5 of the client specifically has been posted there:(93) Recoil Master v5 - My Free Recoil Client - YouTube.
Downloads for old versions of the client are also available on the videos on that youtube channel. Current and legacy parts of the client are all in the RecoilMaster GitHub repository here: kindpump/RecoilMaster.
My website, kypo.org is always updated with the latest public download of the client and also contains links to the discord server, my BuyMeAcoffee page where new updates are posted for supporting members as well as ad-free versions of RecoilMasterv5. There’s also links to my channel from kypo.org as well as my TikTok account.